/*
 * Copyright 2011 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.dspl.xml;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import com.google.dspl.jaxb.AttributeXml;
import com.google.dspl.jaxb.ConceptInfoXml;
import com.google.dspl.jaxb.ConceptInfoXml.Synonym.ConceptValue;
import com.google.dspl.jaxb.ConceptPropertyXml;
import com.google.dspl.jaxb.ConceptTableMappingXml;
import com.google.dspl.jaxb.ConceptTableMappingXml.MapConcept;
import com.google.dspl.jaxb.ConceptTableMappingXml.MapProperty;
import com.google.dspl.jaxb.ConceptXml;
import com.google.dspl.jaxb.DataTypeXml;
import com.google.dspl.jaxb.DataXml;
import com.google.dspl.jaxb.Dspl;
import com.google.dspl.jaxb.Dspl.Concepts;
import com.google.dspl.jaxb.Dspl.Import;
import com.google.dspl.jaxb.Dspl.Slices;
import com.google.dspl.jaxb.Dspl.Tables;
import com.google.dspl.jaxb.Dspl.Topics;
import com.google.dspl.jaxb.InfoXml;
import com.google.dspl.jaxb.SliceConceptRefXml;
import com.google.dspl.jaxb.SliceTableMappingXml;
import com.google.dspl.jaxb.SliceTableMappingXml.MapDimension;
import com.google.dspl.jaxb.SliceTableMappingXml.MapMetric;
import com.google.dspl.jaxb.SliceXml;
import com.google.dspl.jaxb.TableXml;
import com.google.dspl.jaxb.TopicXml;
import com.google.dspl.jaxb.ValueXml;
import com.google.dspl.jaxb.ValuesXml;
import com.google.dspl.model.Attribute;
import com.google.dspl.model.Concept;
import com.google.dspl.model.Concept.ConceptProperty;
import com.google.dspl.model.Concept.ConceptTableMapping;
import com.google.dspl.model.Concept.PropertyMapping;
import com.google.dspl.model.DataType;
import com.google.dspl.model.Dataset;
import com.google.dspl.model.Identifier;
import com.google.dspl.model.Info;
import com.google.dspl.model.Slice;
import com.google.dspl.model.Slice.ConceptMapping;
import com.google.dspl.model.Table;
import com.google.dspl.model.Table.Column;
import com.google.dspl.model.Topic;
import com.google.dspl.model.Value;

import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.namespace.QName;

/**
 * Conversion of a {@link Dataset} to a JAXB {@link Dspl}.
 *
 * @author Shardul Deo
 */
class DatasetToDsplConversion {

  private final NamespaceMapping namespaceMapping;
  private final Dataset dataset;
  private final String targetNamespace;
  private final Set<String> namespaceImports = Sets.newLinkedHashSet();
  private final Ordering<String> translationOrder;

  private int nsPrefix = 1; // Prefix count

  /**
   * @param namespaceMapping Mapping between dataset namespaces and their ids. If the dataset being
   *        converted contains references to external datasets, this map must contain a mapping for
   *        them.
   * @param dataset The dataset to be converted.
   * @param targetNamespace The target namespace of the generated DSPL. If null, generates the DSPL
   *        without a target namespace.
   */
  DatasetToDsplConversion(
      NamespaceMapping namespaceMapping, Dataset dataset, String targetNamespace) {
    this.namespaceMapping = Preconditions.checkNotNull(namespaceMapping);
    this.dataset = Preconditions.checkNotNull(dataset);
    this.targetNamespace = targetNamespace;
    this.translationOrder = Ordering.natural().nullsLast();
  }

  Dspl convert() {
    namespaceImports.clear();
    Dspl dspl = new Dspl();
    if (targetNamespace != null) {
      dspl.setTargetNamespace(targetNamespace);
    }
    if (dataset.getInfo() != null) {
      dspl.setInfo(convertInfo(dataset.getInfo()));
    }
    if (dataset.getProvider() != null) {
      dspl.setProvider(convertInfo(dataset.getProvider()));
    }
    for (Attribute attribute : dataset.getAttributes()) {
      dspl.getAttribute().add(convertAttribute(attribute));
    }

    if (!dataset.getTopics().isEmpty()) {
      dspl.setTopics(new Topics());
    }
    for (Topic topic : dataset.getTopics()) {
      dspl.getTopics().getTopic().add(convertTopic(topic));
    }

    if (!dataset.getConcepts().isEmpty()) {
      dspl.setConcepts(new Concepts());
    }
    for (Concept concept : dataset.getConcepts()) {
      dspl.getConcepts().getConcept().add(convertConcept(concept));
    }

    if (!dataset.getSlices().isEmpty()) {
      dspl.setSlices(new Slices());
    }
    for (Slice slice : dataset.getSlices()) {
      dspl.getSlices().getSlice().add(convertSlice(slice));
    }

    if (!dataset.getTables().isEmpty()) {
      dspl.setTables(new Tables());
    }
    for (Table table : dataset.getTables()) {
      dspl.getTables().getTable().add(convertTable(table));
    }
    for (String namespaceImport : namespaceImports) {
      Import importXml = new Import();
      importXml.setNamespace(namespaceImport);
      dspl.getImport().add(importXml);
    }
    return dspl;
  }

  @VisibleForTesting
  TopicXml convertTopic(Topic topic) {
    TopicXml topicXml = new TopicXml();
    topicXml.setId(topic.getId());
    if (topic.getInfo() != null) {
      topicXml.setInfo(convertInfo(topic.getInfo()));
    }
    if (topic.getParentTopic() != null) {
      topicXml.setParentTopic(convertIdentifier(topic.getParentTopic()));
    }
    for (Topic child : topic.getChildTopics()) {
      topicXml.getTopic().add(convertTopic(child));
    }
    return topicXml;
  }

  @VisibleForTesting
  ConceptXml convertConcept(Concept concept) {
    ConceptXml conceptXml = new ConceptXml();
    conceptXml.setId(concept.getId());
    if (concept.getParent() != null) {
      conceptXml.setExtends(convertIdentifier(concept.getParent()));
    }
    if (concept.getInfo() != null) {
      // TODO: Add plural, total etc. names.
      ConceptInfoXml conceptInfoXml = new ConceptInfoXml();
      populateInfoXml(conceptInfoXml, concept.getInfo());
      conceptXml.setInfo(conceptInfoXml);
    }
    for (Identifier topic : concept.getTopics()) {
      ConceptXml.Topic topicRef = new ConceptXml.Topic();
      topicRef.setRef(convertIdentifier(topic));
      conceptXml.getTopic().add(topicRef);
    }
    if (concept.getType() != null) {
      ConceptXml.Type type = new ConceptXml.Type();
      type.setRef(convertDataType(concept.getType()));
      conceptXml.setType(type);
    }
    for (Attribute attribute : concept.getAttributes()) {
      conceptXml.getAttribute().add(convertAttribute(attribute));
    }
    for (ConceptProperty property : concept.getProperties()) {
      conceptXml.getProperty().add(convertProperty(property));
    }
    if (concept.getDefaultValue() != null) {
      conceptXml.setDefaultValue(convertPrimitiveValue(concept.getDefaultValue()));
    }
    if (concept.getTable() != null) {
      ConceptTableMappingXml conceptTableMappingXml = new ConceptTableMappingXml();
      ConceptTableMapping conceptTableMapping = concept.getTable();
      conceptTableMappingXml.setRef(conceptTableMapping.getTableId());
      if (conceptTableMapping.getConceptMappingColumn() != null) {
        MapConcept mapConcept = new MapConcept();
        mapConcept.setToColumn(conceptTableMapping.getConceptMappingColumn());
        conceptTableMappingXml.setMapConcept(mapConcept);
      }
      convertPropertyMappings(conceptTableMappingXml, conceptTableMapping);
      conceptXml.setTable(conceptTableMappingXml);
    }
    return conceptXml;
  }

  private void convertPropertyMappings(
      ConceptTableMappingXml conceptTableMappingXml, ConceptTableMapping conceptTableMapping) {
    List<PropertyMapping> propertyMappings = conceptTableMapping.getPropertyMappings();
    Ordering<PropertyMapping> mappingSorter = buildPropertyMappingOrdering();
    for (PropertyMapping propertyMapping : mappingSorter.sortedCopy(propertyMappings)) {
      MapProperty mapProperty = new MapProperty();
      mapProperty.setRef(propertyMapping.getPropertyId());
      mapProperty.setToColumn(propertyMapping.getToColumn());
      if (propertyMapping.getLanguage() != null) {
        mapProperty.setLang(propertyMapping.getLanguage());
      }
      conceptTableMappingXml.getMapProperty().add(mapProperty);
    }
  }

  private Ordering<PropertyMapping> buildPropertyMappingOrdering() {
    return new Ordering<PropertyMapping>() {
      @Override
      public int compare(PropertyMapping arg0, PropertyMapping arg1) {
        String language0 = arg0 != null ? arg0.getLanguage() : null;
        String language1 = arg1 != null ? arg1.getLanguage() : null;
        return translationOrder.compare(language0, language1);
      }
    };
  }

  @VisibleForTesting
  ConceptPropertyXml convertProperty(ConceptProperty property) {
    ConceptPropertyXml conceptPropertyXml = new ConceptPropertyXml();
    if (property.getId() != null) {
      conceptPropertyXml.setId(property.getId());
    }
    if (property.getInfo() != null) {
      conceptPropertyXml.setInfo(convertInfo(property.getInfo()));
    }
    if (property.getType() != null) {
      ConceptPropertyXml.Type type = new ConceptPropertyXml.Type();
      type.setRef(convertDataType(property.getType()));
      conceptPropertyXml.setType(type);
    }
    if (property.getConcept() != null) {
      conceptPropertyXml.setConcept(convertIdentifier(property.getConcept()));
    }
    // TODO: Only set this if explicitly set
    conceptPropertyXml.setIsParent(property.isParent());
    conceptPropertyXml.setIsMapping(property.isMapping());
    conceptPropertyXml.setIsRequired(property.isRequired());
    return conceptPropertyXml;
  }

  @VisibleForTesting
  SliceXml convertSlice(Slice slice) {
    SliceXml sliceXml = new SliceXml();
    sliceXml.setId(slice.getId());
    if (slice.getInfo() != null) {
      sliceXml.setInfo(convertInfo(slice.getInfo()));
    }
    for (Identifier dimension : slice.getDimensions()) {
      SliceConceptRefXml sliceConceptRefXml = new SliceConceptRefXml();
      sliceConceptRefXml.setConcept(convertIdentifier(dimension));
      sliceXml.getDimension().add(sliceConceptRefXml);
    }
    for (Identifier metric : slice.getMetrics()) {
      SliceConceptRefXml sliceConceptRefXml = new SliceConceptRefXml();
      sliceConceptRefXml.setConcept(convertIdentifier(metric));
      sliceXml.getMetric().add(sliceConceptRefXml);
    }

    if (slice.getTable() != null) {
      SliceTableMappingXml sliceTableMappingXml = new SliceTableMappingXml();
      sliceTableMappingXml.setRef(new QName(slice.getTable().getTableId()));
      for (ConceptMapping conceptMapping : slice.getTable().getDimensionMappings()) {
        MapDimension mapDimension = new MapDimension();
        mapDimension.setConcept(convertIdentifier(conceptMapping.getConcept()));
        mapDimension.setToColumn(conceptMapping.getToColumn());
        sliceTableMappingXml.getMapDimension().add(mapDimension);
      }
      for (ConceptMapping conceptMapping : slice.getTable().getMetricMappings()) {
        MapMetric mapMetric = new MapMetric();
        mapMetric.setConcept(convertIdentifier(conceptMapping.getConcept()));
        mapMetric.setToColumn(conceptMapping.getToColumn());
        sliceTableMappingXml.getMapMetric().add(mapMetric);
      }
      sliceXml.setTable(sliceTableMappingXml);
    }

    return sliceXml;
  }

  @VisibleForTesting
  TableXml convertTable(Table table) {
    TableXml tableXml = new TableXml();
    tableXml.setId(table.getId());
    if (table.getInfo() != null) {
      tableXml.setInfo(convertInfo(table.getInfo()));
    }
    for (Column column : table.getColumns()) {
      TableXml.Column columnXml = new TableXml.Column();
      columnXml.setId(column.getId());
      if (column.getType() != null) {
        columnXml.setType(convertDataType(column.getType()));
      }
      if (column.getFormat() != null) {
        columnXml.setFormat(column.getFormat());
      }
      if (column.getConstantValue() != null) {
        columnXml.setValue(convertPrimitiveValue(column.getConstantValue()).getValue());
      }
      tableXml.getColumn().add(columnXml);
    }
    if (table.getData() != null) {
      Table.Data data = table.getData();
      DataXml dataXml = new DataXml();
      DataXml.File file = new DataXml.File();
      file.setValue(data.getFile());
      if (data.getFormat() != null) {
        file.setFormat(data.getFormat());
      }
      if (data.getEncoding() != null) {
        file.setEncoding(data.getEncoding());
      }
      dataXml.setFile(file);
      tableXml.setData(dataXml);
    }
    return tableXml;
  }

  @VisibleForTesting
  AttributeXml convertAttribute(Attribute attribute) {
    AttributeXml attributeXml = new AttributeXml();
    if (attribute.getId() != null) {
      attributeXml.setId(attribute.getId());
    }
    if (attribute.getConcept() != null) {
      attributeXml.setConcept(convertIdentifier(attribute.getConcept()));
    }
    if (attribute.getInfo() != null) {
      attributeXml.setInfo(convertInfo(attribute.getInfo()));
    }
    if (attribute.getType() != null) {
      AttributeXml.Type type = new AttributeXml.Type();
      type.setRef(convertDataType(attribute.getType()));
      // TODO: Add format
      attributeXml.setType(type);
    }
    if (attribute.getValue() != null) {
      Value value = attribute.getValue();
      if (value.getConceptValue() != null) {
        ConceptValue conceptValue = new ConceptValue();
        conceptValue.setConcept(convertIdentifier(value.getConceptValue()));
        attributeXml.setConceptValue(conceptValue);
      } else {
        attributeXml.getValue().add(convertPrimitiveValue(value));
      }
    }
    if (!attribute.getStringValues().isEmpty()) {
      attributeXml.getValue().addAll(
          convertMultiLangString(attribute.getStringValues()).getValue());
    }

    return attributeXml;
  }

  private DataTypeXml convertDataType(DataType dataType) {
    return DataTypeXml.valueOf(dataType.name());
  }

  @VisibleForTesting
  ValueXml convertPrimitiveValue(Value value) {
    ValueXml valueXml = new ValueXml();
    if (value.getStringValue() != null) {
      valueXml.setValue(value.getStringValue());
    } else if (value.getFloatValue() != null) {
      valueXml.setValue(value.getFloatValue().toString());
    } else if (value.getIntegerValue() != null) {
      valueXml.setValue(value.getIntegerValue().toString());
    } else if (value.getBooleanValue() != null) {
      valueXml.setValue(value.getBooleanValue().toString());
    } else if (value.getDateValue() != null) {
      // TODO: Is this the best format?
      valueXml.setValue(value.getDateValue().toString());
    } else if (value.getConceptValue() != null) {
      valueXml.setValue(convertIdentifier(value.getConceptValue()).toString());
    }
    return valueXml;
  }

  @VisibleForTesting
  QName convertIdentifier(Identifier identifier) {
    if (identifier.getDatasetId().equals(dataset.getDatasetId())) {
      return new QName(identifier.getObjectId());
    } else {
      String namespace = namespaceMapping.getNamespaceForDatasetId(identifier.getDatasetId());
      if (namespace == null) {
        throw new IllegalArgumentException("Cannot resolve reference to external dataset: "
            + identifier.getDatasetId());
      }
      namespaceImports.add(namespace);
      return new QName(namespace, identifier.getObjectId(), "prefix" + nsPrefix++);
    }
  }

  @VisibleForTesting
  InfoXml convertInfo(Info info) {
    InfoXml infoXml = new InfoXml();
    populateInfoXml(infoXml, info);
    return infoXml;
  }

  @VisibleForTesting
  void populateInfoXml(InfoXml infoXml, Info info) {
    if (!info.getNames().isEmpty()) {
      infoXml.setName(convertMultiLangString(info.getNames()));
    }
    if (!info.getDescriptions().isEmpty()) {
      infoXml.setDescription(convertMultiLangString(info.getDescriptions()));
    }
    if (!info.getUrls().isEmpty()) {
      infoXml.setUrl(convertMultiLangString(info.getUrls()));
    }
  }

  @VisibleForTesting
  ValuesXml convertMultiLangString(Map<String, String> values) {
    ValuesXml valuesXml = new ValuesXml();
    for (String language : translationOrder.sortedCopy(values.keySet())) {
      ValueXml valueXml = new ValueXml();
      valueXml.setLang(language);
      valueXml.setValue(values.get(language));
      valuesXml.getValue().add(valueXml);
    }
    return valuesXml;
  }
}
